﻿using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace DomainLoginTest
{
    public class JWTValidateHanlder
    {
        readonly string _token;
        readonly X509Certificate2 _x509Certificate2;
        private readonly JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
        /// <summary>
        /// 
        /// </summary>
        /// <param name="token">The access token</param>
        /// <param name="certificate2">The certificate</param>
        public JWTValidateHanlder(string token, X509Certificate2 certificate2)
        {
            this._token = token;
            this._x509Certificate2 = certificate2;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="token">The access token</param>
        /// <param name="certPath">The certificate path</param>
        /// <param name="pwd">The certificate access password</param>
        public JWTValidateHanlder(string token, string certPath, string pwd)
              : this(token, new X509Certificate2(certPath, pwd))
        {

        }

        /// <summary>
        /// Validate the jwt token
        /// </summary>
        /// <param name="tokenParam">
        /// The jwt token parameters which will be validated.
        /// The Issuer,Audience will not be validated default.
        /// If you want to validate them,please set ValidateIssuer=true, ValidIssuer="your issuer",ValidateAudience=true,ValidAudience="your audice"
        /// </param>
        /// <returns>The TokenValidationResult</returns>
        public TokenValidationResult ValidateJWT(Action<TokenValidationParameters> tokenParam = null)
        {
            TokenValidationResult result = new TokenValidationResult();
            bool paramIsValid = ValidateParameters(out string msg);
            if (!paramIsValid)
            {
                result.IsValid = false;
                result.Exception = new ArgumentException(msg);
            }
            else
            {
                try
                {
                    var key = new X509SecurityKey(_x509Certificate2);
                    TokenValidationParameters parameters = new TokenValidationParameters
                    {
                        IssuerSigningKey = key,
                        ValidateIssuer = false,
                        ValidateAudience = false,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        //ClockSkew = TimeSpan.FromSeconds(0)
                    };

                    tokenParam?.Invoke(parameters);
                    var claimsPrincipal = handler.ValidateToken(_token, parameters, out SecurityToken validatedToken);
                    result.IsValid = true;
                    result.SecurityToken = validatedToken;
                    result.ClaimsIdentity = new ClaimsIdentity(claimsPrincipal.Identity);
                }
                catch (Exception ex)
                {
                    result.IsValid = false;
                    result.Exception = ex;
                }
            }

            return result;
        }

        private bool ValidateParameters(out string message)
        {
            var flag = true;
            message = string.Empty;
            if (string.IsNullOrEmpty(this._token) || this._x509Certificate2 == null)
            {
                message = "params can not be null";
                flag = false;
            }
            else
            {
                if (!handler.CanReadToken(this._token))
                {
                    message = "token is invalid";
                    flag = false;
                }
            }

            return flag;
        }

        /// <summary> 
        /// Validate JWT Signature only.It means will not validate issuer,lefttime,audience etc.       
        /// </summary>
        /// <param name="message">The validate message.If true,the message will be empty</param>
        /// <returns></returns>
        public bool ValidateJWTSignatureOnly(out string message)
        {
            var result = ValidateParameters(out string msg);
            if (result)
            {
                var tokenParts = this._token.Split('.');
                result = ValidateSign(tokenParts[0], tokenParts[1], tokenParts[2], () => _x509Certificate2.GetRSAPublicKey().ToXmlString(false));
                msg = !result ? "Signature is invalida" : string.Empty;
            }
            message = msg;
            return result;
        }

        private bool ValidateSign(string header, string payload, string sign, Func<string> pubKey)
        {
            if (string.IsNullOrEmpty(header) ||
                string.IsNullOrEmpty(payload) ||
                string.IsNullOrEmpty(sign) ||
                string.IsNullOrEmpty(pubKey?.Invoke()))
            {
                return false;
            }

            RSACryptoServiceProvider rSA = new RSACryptoServiceProvider();
            rSA.FromXmlString(pubKey.Invoke());
            SHA256 sHA = SHA256.Create();
            var hash = sHA.ComputeHash(Encoding.UTF8.GetBytes($"{header}.{payload}"));

            RSAPKCS1SignatureDeformatter signatureDeformatter = new RSAPKCS1SignatureDeformatter(rSA);
            signatureDeformatter.SetHashAlgorithm("SHA256");

            return signatureDeformatter.VerifySignature(hash, FromBase64Url(sign));
        }

        private byte[] FromBase64Url(string base64Url)
        {
            string padded = base64Url.Length % 4 == 0
                ? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
            string base64 = padded.Replace("_", "/")
                                  .Replace("-", "+");
            return Convert.FromBase64String(base64);
        }
    }
}
